﻿namespace Contoso.ActiveDirectory {

    #region Usings
    using System;
    using System.Collections.Generic;
    using System.Diagnostics;
    using System.Globalization;
    using System.IO;
    using System.Linq;
    using System.Text;
    using System.Threading;
    using System.Threading.Tasks;
    #endregion

    /// <summary>
    /// Provides information on the following User account tabs: Environment, Sessions, Remote Control, Remote Desktop Services Profile
    /// 
    /// UserParameters attribute:
    /// https://msdn.microsoft.com/en-us/library/ff635189.aspx
    /// 
    /// TSProperty structure:
    /// https://msdn.microsoft.com/en-us/library/ff635169.aspx
    /// 
    /// USERCONFIG structure:
    /// https://msdn.microsoft.com/en-us/library/cc248610.aspx
    /// 
    /// The userParameters attribute is used by Terminal Services to store the configuration data associated with the user connected to a 
    /// session running on a terminal server. This configuration data is returned in a USERCONFIG structure by the RpcGetConfigData method.
    /// Terminal Services does not use UTF-8 or UTF-16 encoding to store the configuration data in the userParameters attribute. 
    /// Terminal Services stores the user configuration data in the userParameters attribute in the following format:
    /// 
    /// ReservedData (96 bytes): A 96-byte array of reserved data.<148>
    /// Signature (2 bytes): A 2-byte Unicode character. This field is used by Terminal Services to assert the validity of the 
    ///  TSPropertyCount and TSPropertyArray fields. Terminal Services compares the data contained in this field with Unicode character "P".
    ///  If this field contains the aforementioned character, then the information inside the TSPropertyArray and TSPropertyCount fields 
    ///  is considered valid. If it contains a different value, then all information inside the TSPropertyCount and TSPropertyArray fields 
    ///  is considered invalid.
    /// TSPropertyCount (2 bytes): A 2-byte unsigned integer indicating the number of elements in TSPropertyArray.
    /// TSPropertyArray (variable): A variable-length array of TSProperty structures. The number of elements in this array is specified by the field TSPropertyCount.
    /// </summary>
    [Serializable]
    public class RemoteDesktopAttributes : IRemoteDesktopAttributes {

        #region Members
        public static readonly string[] AttributesToGet;

        /// <summary>
        /// Flags of the values that may be present in the CtxCfgFlags1 TSProperty of the userParameters attribute
        /// </summary>
        /// Default value when userParameters is not present: 
        /// CtxCfgFlags1.F1MSK_DISABLEENCRYPTION | CtxCfgFlags1.F1MSK_FORCECLIENTLPTDEF | CtxCfgFlags1.F1MSK_AUTOCLIENTLPTS 
        /// | CtxCfgFlags1.F1MSK_AUTOCLIENTDRIVES | CtxCfgFlags1.F1MSK_INHERITAUTOCLIENT | CtxCfgFlags1.F1MSK_INHERITINITIALPROGRAM 
        /// | CtxCfgFlags1.Unknown80000000;
        public CtxCfgFlags1 CtxCfgFlags1 { get; private set; }

        /// <summary>
        /// The minimum allowed encryption level of the user session
        /// </summary>
        public CtxEncryptionLevel CtxMinEncryptionLevel { get; set; }

        /// <summary>
        /// Returns if the current configuration settings are equal to the default configuration where userParameters is clear/not specified
        /// </summary>
        public bool IsDefaultSettings {
            get {
                return this.IsValid
                    && this.CtxCfgFlags1.HasFlag(CtxCfgFlags1.F1MSK_DISABLEENCRYPTION)
                    && this.CtxCfgFlags1.HasFlag(CtxCfgFlags1.F1MSK_FORCECLIENTLPTDEF)
                    && this.CtxCfgFlags1.HasFlag(CtxCfgFlags1.F1MSK_AUTOCLIENTLPTS)
                    && this.CtxCfgFlags1.HasFlag(CtxCfgFlags1.F1MSK_AUTOCLIENTDRIVES)
                    && this.CtxCfgFlags1.HasFlag(CtxCfgFlags1.F1MSK_INHERITAUTOCLIENT)
                    && this.CtxCfgFlags1.HasFlag(CtxCfgFlags1.F1MSK_INHERITINITIALPROGRAM)
                    && this.CtxCfgFlags1.HasFlag(CtxCfgFlags1.Unknown80000000)
                    && (this.CtxMinEncryptionLevel == CtxEncryptionLevel.Low)
                    && (this.DisconnectAction == RemoteDesktopDisconnectAction.Disconnect)
                    && (this.MaxConnectionTime == TimeSpan.Zero)
                    && (this.MaxDisconnectionTime == TimeSpan.Zero)
                    && (this.MaxIdleTime == TimeSpan.Zero)
                    && (this.ReconnectAction == RemoteDesktopReconnectAction.FromAnyClient)
                    && (this.RemoteControlSettings == RemoteControlSettings.EnableInputNotify)
                    && string.IsNullOrWhiteSpace(this.RemoteDesktopHomeDirectory)
                    && string.IsNullOrWhiteSpace(this.RemoteDesktopHomeDrive)
                    && string.IsNullOrWhiteSpace(this.RemoteDesktopInitialProgram)
                    && string.IsNullOrWhiteSpace(this.RemoteDesktopProfilePath)
                    && string.IsNullOrWhiteSpace(this.RemoteDesktopWorkDirectory);
            }
        }

        /// <summary>
        /// Set to false if the signature of the userParameters attribute is not the Unicode character "P"
        /// or an error occurs during decoding, or the userParameters length is not sufficient to hold any data
        /// </summary>
        public bool IsValid {
            get {
                return isValid
                    && (this.CtxMinEncryptionLevel != CtxEncryptionLevel.Undefined)
                    && (this.DisconnectAction != RemoteDesktopDisconnectAction.Undefined)
                    && (this.ReconnectAction != RemoteDesktopReconnectAction.Undefined);
            }
            private set {
                isValid = value;
            }
        }
        private bool isValid;

        /// <summary>
        /// True if userParameters attribute was presented for processing
        /// </summary>
        public bool IsPresent { get; private set; }

        /// <summary>
        /// The parent object that instantiated this class
        /// </summary>
        private object ParentObject { get; set; }

        /// <summary>
        /// The minimum length of the userParameters blob (96 bytes reserved, 2 bytes signature, plus some bytes for data)
        /// </summary>
        private static readonly int UserParametersBlobMinimumLenth = 100;

        #region Remote Desktop Services Profile tab
        /// <summary>
        /// Specifies to allow or deny logon to a Remote Desktop Server
        /// Default value when userParameters is not present: TRUE
        /// </summary>
        public bool AllowLogon {
            get {
                return !this.CtxCfgFlags1.HasFlag(CtxCfgFlags1.F1MSK_LOGONDISABLED);
            }
            set {
                if (value) {
                    this.CtxCfgFlags1 &= ~CtxCfgFlags1.F1MSK_LOGONDISABLED;
                }
                else {
                    this.CtxCfgFlags1 |= CtxCfgFlags1.F1MSK_LOGONDISABLED;
                }
            }
        }

        /// <summary>
        /// The home directory for the account when logged on to a  Remote Desktop Server
        /// </summary>
        public string RemoteDesktopHomeDirectory { get; set; }

        /// <summary>
        /// The home drive for the account when logged on to a  Remote Desktop Server
        /// </summary>
        public string RemoteDesktopHomeDrive { get; set; }

        /// <summary>
        /// The user profile for the account when logged on to a  Remote Desktop Server
        /// </summary>
        public string RemoteDesktopProfilePath { get; set; }
        #endregion

        #region Remote Control tab
        /// <summary>
        /// Specifies if remote control is enabled or disabled for an account
        /// Default value when userParameters is not present: TRUE
        /// </summary>
        public bool RemoteControlEnabled {
            get {
                return this.RemoteControlSettings != RemoteControlSettings.Disable;
            }
            set {
                if (value) {
                    // default value
                    this.RemoteControlSettings = RemoteControlSettings.EnableInputNotify;
                }
                else {
                    this.RemoteControlSettings = RemoteControlSettings.Disable;
                }
            }
        }

        /// <summary>
        /// Specifies the value of the settings for input and permission on the user account Remote Control tab
        /// Default value when userParameters is not present: EnableInputNotify
        /// </summary>
        public RemoteControlSettings RemoteControlSettings { get; set; }
        #endregion

        #region Sessions tab
        /// <summary>
        /// Specifies the action to take (disconnect or logoff) when a connection is cut off
        /// Default value when userParameters is not present: Disconnect
        /// </summary>
        public RemoteDesktopDisconnectAction DisconnectAction {
            get {
                return this.CtxCfgFlags1.HasFlag(CtxCfgFlags1.F1MSK_RESETBROKEN)
                    ? RemoteDesktopDisconnectAction.Logoff
                    : RemoteDesktopDisconnectAction.Disconnect;
            }
            set {
                if (value == RemoteDesktopDisconnectAction.Logoff) {
                    this.CtxCfgFlags1 |= CtxCfgFlags1.F1MSK_RESETBROKEN;
                }
                else if (value == RemoteDesktopDisconnectAction.Disconnect) {
                    this.CtxCfgFlags1 &= ~CtxCfgFlags1.F1MSK_RESETBROKEN;
                }
            }
        }

        /// <summary>
        /// Specifies the amount of time a session may be connected
        /// Default value when userParameters is not present: Not specified
        /// </summary>
        public TimeSpan MaxConnectionTime { get; set; }

        /// <summary>
        /// Specifies the amount of time a session may be disconnected before it is logged off
        /// Default value when userParameters is not present: Not specified
        /// </summary>
        public TimeSpan MaxDisconnectionTime { get; set; }

        /// <summary>
        /// Specifies the amount of time a session may be idle (no activity)
        /// Default value when userParameters is not present: Not specified
        /// </summary>
        public TimeSpan MaxIdleTime { get; set; }

        /// <summary>
        /// Specifies the behavior when a session is reconnected
        /// Default value when userParameters is not present: FromAnyClient
        /// </summary>
        public RemoteDesktopReconnectAction ReconnectAction {
            get {
                return this.CtxCfgFlags1.HasFlag(CtxCfgFlags1.F1MSK_RECONNECTSAME)
                    ? RemoteDesktopReconnectAction.FromOriginatingClientOnly
                    : RemoteDesktopReconnectAction.FromAnyClient;
            }
            set {
                if (value == RemoteDesktopReconnectAction.FromOriginatingClientOnly) {
                    this.CtxCfgFlags1 |= ActiveDirectory.CtxCfgFlags1.F1MSK_RECONNECTSAME;
                }
                else if (value == RemoteDesktopReconnectAction.FromAnyClient) {
                    this.CtxCfgFlags1 &= ~CtxCfgFlags1.F1MSK_RECONNECTSAME;
                }
            }
        }
        #endregion

        #region Environment tab
        /// <summary>
        /// Specifies to connect client drives at logon
        /// Default value when userParameters is not present: TRUE
        /// </summary>
        public bool ConnectClientDrivesAtLogon {
            get {
                return this.CtxCfgFlags1.HasFlag(CtxCfgFlags1.F1MSK_AUTOCLIENTDRIVES);
            }
            set {
                if (value) {
                    this.CtxCfgFlags1 |= CtxCfgFlags1.F1MSK_AUTOCLIENTDRIVES;
                }
                else {
                    this.CtxCfgFlags1 &= ~CtxCfgFlags1.F1MSK_AUTOCLIENTDRIVES;
                }
            }
        }

        /// <summary>
        /// Specifies to connect client printers at logon
        /// Default value when userParameters is not present: TRUE
        /// </summary>
        public bool ConnectClientPrintersAtLogon {
            get {
                return this.CtxCfgFlags1.HasFlag(CtxCfgFlags1.F1MSK_AUTOCLIENTLPTS);
            }
            set {
                if (value) {
                    this.CtxCfgFlags1 |= CtxCfgFlags1.F1MSK_AUTOCLIENTLPTS;
                }
                else {
                    this.CtxCfgFlags1 &= ~CtxCfgFlags1.F1MSK_AUTOCLIENTLPTS;
                }
            }
        }

        /// <summary>
        /// Specifies to default to main client printer at logon
        /// Default value when userParameters is not present: TRUE
        /// </summary>
        public bool DefaultToMainPrinter {
            get {
                return this.CtxCfgFlags1.HasFlag(CtxCfgFlags1.F1MSK_FORCECLIENTLPTDEF);
            }
            set {
                if (value) {
                    this.CtxCfgFlags1 |= ActiveDirectory.CtxCfgFlags1.F1MSK_FORCECLIENTLPTDEF;
                }
                else {
                    this.CtxCfgFlags1 &= ~CtxCfgFlags1.F1MSK_FORCECLIENTLPTDEF;
                }
            }
        }

        /// <summary>
        /// The initial program to run at logon (typically for captive accounts)
        /// </summary>
        public string RemoteDesktopInitialProgram { get; set; }

        /// <summary>
        /// The working directory for the initial program to run at logon 
        /// </summary>
        public string RemoteDesktopWorkDirectory { get; set; }
        #endregion
        #endregion

        #region Constructors
        static RemoteDesktopAttributes() {

            AttributesToGet = new string[]
            {
                "userParameters"
            };
        }

        public RemoteDesktopAttributes() {
            // Set values to the default as if userParameters attribute is not specified
            this.CtxCfgFlags1 = CtxCfgFlags1.F1MSK_DISABLEENCRYPTION
                | CtxCfgFlags1.F1MSK_FORCECLIENTLPTDEF
                | CtxCfgFlags1.F1MSK_AUTOCLIENTLPTS
                | CtxCfgFlags1.F1MSK_AUTOCLIENTDRIVES
                | CtxCfgFlags1.F1MSK_INHERITAUTOCLIENT
                | CtxCfgFlags1.F1MSK_INHERITINITIALPROGRAM
                | CtxCfgFlags1.Unknown80000000;
            this.CtxMinEncryptionLevel = CtxEncryptionLevel.Low;
            this.RemoteControlSettings = RemoteControlSettings.EnableInputNotify;
            this.RemoteDesktopHomeDirectory = string.Empty;
            this.RemoteDesktopHomeDrive = string.Empty;
            this.RemoteDesktopInitialProgram = string.Empty;
            this.RemoteDesktopProfilePath = string.Empty;
            this.RemoteDesktopWorkDirectory = string.Empty;
            this.IsValid = true;
            this.ParentObject = "N/A";
        }

        public RemoteDesktopAttributes(object parentObject)
            : this() {
                this.ParentObject = parentObject ?? "N/A";
        }
        #endregion

        #region Methods

        /// <summary>
        /// Decodes the characters for a TSPropertyValue string in a userParameters attribute
        /// To create the encoded binary BLOB for the PropValue field, for each byte of the input create its ASCII-encoded 
        /// hexadecimal representation and place this representation in 2 consecutive bytes of the output buffer, the most 
        /// significant hexadecimal digit first followed by the least significant hexadecimal digit. 
        /// For example, if the input byte contains the ASCII representation of character 'A', the resulting output will be 
        /// a sequence of two ASCII characters: character '4' followed by character '1' because the hexadecimal representation 
        /// of a byte that contains the ASCII character 'A' is 41. Hence, the output buffer corresponding to the input buffer 
        /// byte containing character 'A' will be a sequence of 2 bytes whose hexadecimal representations are 34 and 31. 
        /// As another example, the input buffer containing the ASCII string "ABCDE\0" would be encoded into the ASCII 
        /// string "414243444500" (without the terminating 0), which is the same as a sequence of 12 bytes whose hexadecimal 
        /// representations are 34, 31, 34, 32, 34, 33, 34, 34, 34, 35, 30, and 30.
        /// 
        /// Encoding PropValue Field in TSProperty Structure
        /// https://msdn.microsoft.com/en-us/library/ff635243.aspx
        /// </summary>
        /// <param name="propertyValueChars">The char[] of a TSPropertyValue</param>
        /// <returns>The decoded string</returns>
        private string DecodeTSPropertyChars(char[] propertyValueChars, bool breakOnNull = true) {

            var charPairsDecoded = new StringBuilder();
            if ((propertyValueChars != null) && (propertyValueChars.Length > 1)) {
                // need to get chars in pairs.  The char pairs form a hex number
                string charPairs = new string(propertyValueChars);

                if (!string.IsNullOrWhiteSpace(charPairs) && charPairs.Length > 1) {
                    for (int indexcharPairs = 0; indexcharPairs < (charPairs.Length - 1); indexcharPairs += 2) {

                        string charPairHex = charPairs.Substring(indexcharPairs, 2);
                        int charPairHexNumber = int.Parse(charPairHex, NumberStyles.AllowHexSpecifier);
                        // if charPair is 00, that is NULL/End of data - break;
                        if (breakOnNull && (charPairHexNumber == 0)) break;
                        char charDecoded = (char)charPairHexNumber;
                        charPairsDecoded.Append(charDecoded);
                    }
                }
            }

            return charPairsDecoded.ToString();
        }

        /// <summary>
        /// Decodes the characters for a TSPropertyValue number in a userParameters attribute
        /// </summary>
        /// <param name="propertyValueChars">The char[] of a TSPropertyValue</param>
        /// <returns></returns>
        private string DecodeTSPropertyNumber(char[] propertyValueChars, bool flipCharPairs = false) {

            var charPairsDecoded = new StringBuilder();
            if ((propertyValueChars != null) && (propertyValueChars.Length > 1)) {
                // Reverse the array - Big Endian
                Array.Reverse(propertyValueChars);
                string charPairs = new string(propertyValueChars);
                if (flipCharPairs) {
                    // get chars in pairs, then flip each pair
                    if (!string.IsNullOrWhiteSpace(charPairs) && (charPairs.Length > 1)) {
                        for (int indexcharPairs = 0; indexcharPairs < (charPairs.Length - 1); indexcharPairs += 2) {

                            string charPair = charPairs.Substring(indexcharPairs, 2);
                            string charPairFlipped = new string(charPair.Reverse().ToArray());
                            charPairsDecoded.Append(charPairFlipped);
                        }
                    }
                }
                else {
                    charPairsDecoded.Append(charPairs);
                }
            }

            return charPairsDecoded.ToString();
        }

        /// <summary>
        /// Encodes the string value of the number to a TSProperty value
        /// </summary>
        /// <param name="number">The number string</param>
        /// <returns>The byte array of the TSProperty value</returns>
        private byte[] EncodeTSPropertyChars(string propertyValue) {
            if (string.IsNullOrWhiteSpace(propertyValue)) throw new ArgumentNullException("propertyValue");

            var encodedChars = new StringBuilder();
            foreach (var unencodedChar in propertyValue) {
                int charNumericValue = (int)unencodedChar;
                string unencodedCharHexNumber = string.Format("{0:x}", (int)charNumericValue);
                // must be two digits - ASCII only
                if (unencodedCharHexNumber.Length != 2) {
                    throw new ApplicationException(string.Format("Unexpected character: {0} numeric value: {1}", unencodedChar, unencodedCharHexNumber));
                }

                encodedChars.Append(unencodedCharHexNumber.ToCharArray());

                // append NULL character pair to end of string (00)
                encodedChars.Append((char)48);
                encodedChars.Append((char)48);
            }

            return Encoding.ASCII.GetBytes(encodedChars.ToString());
        }

        /// <summary>
        /// Encodes the string value of the number to a TSProperty value
        /// </summary>
        /// <param name="hexNumber">The number string</param>
        /// <returns>The byte array of the TSProperty value</returns>
        private byte[] EncodeTSPropertyNumber(string hexNumber) {
            if (string.IsNullOrWhiteSpace(hexNumber)) throw new ArgumentNullException("hexNumber");
            var numberFlippedChars = new List<char>();

            uint testNumber = 0;
            if (uint.TryParse(hexNumber, NumberStyles.AllowHexSpecifier, CultureInfo.InvariantCulture, out testNumber)) {
                for (int indexcharPairs = 0; indexcharPairs < (hexNumber.Length - 1); indexcharPairs += 2) {

                    string charPair = hexNumber.Substring(indexcharPairs, 2);
                    string charPairFlipped = new string(charPair.Reverse().ToArray());
                    numberFlippedChars.AddRange(charPairFlipped.ToCharArray());
                }
            }
            else {
                throw new ApplicationException(string.Format("String is not a valid unsigned integer: {0}", hexNumber));
            }

            byte[] encodedBytes = Encoding.ASCII.GetBytes(numberFlippedChars.ToArray());
            Array.Reverse(encodedBytes);

            return encodedBytes;
        }

        /// <summary>
        /// Creates a userParameters string based on the properties of this object
        /// </summary>
        /// <returns>The userParameters string</returns>
        /// <remarks>
        /// Not necessary to use this if this.IsDefaultSettings is true.  In that case, the userParameters attribute should be removed.
        /// </remarks>
        public string GenerateUserParameters() {
            var userParametersBytes = new List<byte>();
            // if not valid, return an empty string to indicate that the userParameters attribute should be cleared
            if (!this.IsValid) return string.Empty;

            // create reserved and signature headers
            char[] reserved = new char[48];
            for (int index = 0; index < 48; index++) {
                reserved[index] = ' ';
            }
            char[] signature = new char[1] { 'P' };

            userParametersBytes.AddRange(Encoding.Unicode.GetBytes(reserved));
            userParametersBytes.AddRange(Encoding.Unicode.GetBytes(signature));

            // remaining data is array of TSProperty structures

            var tsProperties = new List<byte[]>();

            // Add TSProperty: CtxCfgPresent Value: B00B1E55
            var ctxCfgPresentEncodedValue = this.EncodeTSPropertyNumber("b00b1e55");
            var ctxCfgPresentTSProperty = this.GenerateTSProperty("CtxCfgPresent", ctxCfgPresentEncodedValue);
            tsProperties.Add(ctxCfgPresentTSProperty);

            // Add TSProperty: CtxCfgFlags1
            var ctxCfgFlags1TextValue = string.Format("{0:x8}", (uint)this.CtxCfgFlags1);
            var ctxCfgFlags1EncodedValue = this.EncodeTSPropertyNumber(ctxCfgFlags1TextValue);
            var ctxCfgFlags1TSProperty = this.GenerateTSProperty("CtxCfgFlags1", ctxCfgFlags1EncodedValue);
            tsProperties.Add(ctxCfgFlags1TSProperty);

            // Add TSProperty: CtxShadow
            var ctxShadowTextValue = string.Format("{0:x8}", (uint)this.RemoteControlSettings);
            var ctxShadowEncodedValue = this.EncodeTSPropertyNumber(ctxShadowTextValue);
            var ctxShadowTSProperty = this.GenerateTSProperty("CtxShadow", ctxShadowEncodedValue);
            tsProperties.Add(ctxShadowTSProperty);

            // Add TSProperty: CtxMaxConnectionTime
            if (this.MaxConnectionTime != TimeSpan.Zero) {
                var ctxMaxConnectionTimeTextValue = string.Format("{0:x8}", (uint)this.MaxConnectionTime.TotalMilliseconds);
                var ctxMaxConnectionTimeEncodedValue = this.EncodeTSPropertyNumber(ctxMaxConnectionTimeTextValue);
                var ctxMaxConnectionTimeTSProperty = this.GenerateTSProperty("CtxMaxConnectionTime", ctxMaxConnectionTimeEncodedValue);
                tsProperties.Add(ctxMaxConnectionTimeTSProperty);
            }

            // Add TSProperty: CtxMaxDisconnectionTime
            if (this.MaxDisconnectionTime != TimeSpan.Zero) {
                var ctxMaxDisconnectionTimeTextValue = string.Format("{0:x8}", (uint)this.MaxDisconnectionTime.TotalMilliseconds);
                var ctxMaxDisconnectionTimeEncodedValue = this.EncodeTSPropertyNumber(ctxMaxDisconnectionTimeTextValue);
                var ctxMaxDisconnectionTimeTSProperty = this.GenerateTSProperty("CtxMaxDisconnectionTime", ctxMaxDisconnectionTimeEncodedValue);
                tsProperties.Add(ctxMaxDisconnectionTimeTSProperty);
            }

            // Add TSProperty: CtxMaxIdleTime
            if (this.MaxIdleTime != TimeSpan.Zero) {
                var ctxMaxIdleTimeTextValue = string.Format("{0:x8}", (uint)this.MaxIdleTime.TotalMilliseconds);
                var ctxMaxIdleTimeEncodedValue = this.EncodeTSPropertyNumber(ctxMaxIdleTimeTextValue);
                var ctxMaxIdleTimeTSProperty = this.GenerateTSProperty("CtxMaxIdleTime", ctxMaxIdleTimeEncodedValue);
                tsProperties.Add(ctxMaxIdleTimeTSProperty);
            }

            // Add TSProperty: CtxMinEncryptionLevel
            var ctxMinEncryptionLevelTextValue = string.Format("{0:x2}", (short)this.CtxMinEncryptionLevel);
            var ctxMinEncryptionLevelEncodedValue = this.EncodeTSPropertyNumber(ctxMinEncryptionLevelTextValue);
            var ctxMinEncryptionLevelTSProperty = this.GenerateTSProperty("CtxMinEncryptionLevel", ctxMinEncryptionLevelEncodedValue);
            tsProperties.Add(ctxMinEncryptionLevelTSProperty);

            // Add TSProperty: CtxWorkDirectory
            if (!string.IsNullOrWhiteSpace(this.RemoteDesktopWorkDirectory)) {
                var ctxWorkDirectoryEncodedValue = this.EncodeTSPropertyChars(this.RemoteDesktopWorkDirectory);
                var ctxWorkDirectoryTSProperty = this.GenerateTSProperty("CtxWorkDirectory", ctxWorkDirectoryEncodedValue);
                tsProperties.Add(ctxWorkDirectoryTSProperty);
            }

            // Add TSProperty: CtxWFHomeDir
            if (!string.IsNullOrWhiteSpace(this.RemoteDesktopHomeDirectory)) {
                var ctxWFHomeDirEncodedValue = this.EncodeTSPropertyChars(this.RemoteDesktopHomeDirectory);
                var ctxWFHomeDirTSProperty = this.GenerateTSProperty("CtxWFHomeDir", ctxWFHomeDirEncodedValue);
                tsProperties.Add(ctxWFHomeDirTSProperty);
            }

            // Add TSProperty: CtxWFHomeDirDrive
            if (!string.IsNullOrWhiteSpace(this.RemoteDesktopHomeDrive) && (this.RemoteDesktopHomeDrive.Length == 2)) {
                var ctxWFHomeDirDriveEncodedValue = this.EncodeTSPropertyChars(this.RemoteDesktopHomeDrive);
                var ctxWFHomeDirDriveTSProperty = this.GenerateTSProperty("CtxWFHomeDirDrive", ctxWFHomeDirDriveEncodedValue);
                tsProperties.Add(ctxWFHomeDirDriveTSProperty);
            }

            // Add TSProperty: CtxWFProfilePath
            if (!string.IsNullOrWhiteSpace(this.RemoteDesktopProfilePath)) {
                var ctxWFProfilePathEncodedValue = this.EncodeTSPropertyChars(this.RemoteDesktopProfilePath);
                var ctxWFProfilePathTSProperty = this.GenerateTSProperty("CtxWFProfilePath", ctxWFProfilePathEncodedValue);
                userParametersBytes.AddRange(ctxWFProfilePathTSProperty);
            }

            // Add TSProperty: CtxInitialProgram
            if (!string.IsNullOrWhiteSpace(this.RemoteDesktopInitialProgram)) {
                var ctxInitialProgramEncodedValue = this.EncodeTSPropertyChars(this.RemoteDesktopInitialProgram);
                var ctxInitialProgramTSProperty = this.GenerateTSProperty("CtxInitialProgram", ctxInitialProgramEncodedValue);
                tsProperties.Add(ctxInitialProgramTSProperty);
            }

            // Add tsPropertyCount
            userParametersBytes.AddRange(BitConverter.GetBytes((ushort)tsProperties.Count));

            foreach (var tsProperty in tsProperties) {
                userParametersBytes.AddRange(tsProperty);
            }

            return Encoding.Unicode.GetString(userParametersBytes.ToArray());
        }

        /// <summary>
        /// Creates a TSProperty structure based on the supplied values
        /// NameLength (2 bytes): A 2-byte unsigned integer indicating the length of the PropName field in bytes.
        /// ValueLength (2 bytes): A 2-byte unsigned integer indicating the length of the PropValue field in bytes.
        /// Type (2 bytes): A 2-byte unsigned integer indicating the type of the PropValue field. 
        /// TSPropertyName (variable length): A Unicode string whose length is indicated by the NameLength field. The string indicates the name of the property.
        /// TSPropertyValue (variable length): PropValue (variable): An encoded binary BLOB of length indicated by the ValueLength field. 
        ///  This field either contains a 32-bit unsigned integer or an array of ASCII characters. 
        /// </summary>
        /// <param name="tsPropertyName">The TSProperty name</param>
        /// <param name="tsPropertyValue">The TSProperty value</param>
        /// <returns>The character array of the TSProperty</returns>
        private byte[] GenerateTSProperty(string tsPropertyName, byte[] tsPropertyValue) {

            byte[] tsPropertyNameBytes = Encoding.Unicode.GetBytes(tsPropertyName);

            //  Concatenate nameLength, valueLength, valueType (1), tsPropertyNameBytes, tsPropertyValueBytes
            var tsProperty = new List<byte>();

            tsProperty.AddRange(BitConverter.GetBytes((ushort)tsPropertyNameBytes.Length));
            tsProperty.AddRange(BitConverter.GetBytes((ushort)tsPropertyValue.Length));
            tsProperty.AddRange(BitConverter.GetBytes((ushort)1));
            tsProperty.AddRange(tsPropertyNameBytes);
            tsProperty.AddRange(tsPropertyValue);

            return tsProperty.ToArray();
        }

        /// <summary>
        /// Process the results of the supplied attributes
        /// </summary>
        /// <param name="attributes">The attributes to process</param>
        public void ProcessResults(Dictionary<string, object[]> attributes) {

            try {

                // Decode userParameters attribute
                if (attributes.ContainsKey("userParameters")) {
                    this.IsPresent = true;
                    byte[] userParametersBlob = Encoding.Unicode.GetBytes(attributes["userParameters"][0].ToString());
                    if ((userParametersBlob != null) && (userParametersBlob.Length > UserParametersBlobMinimumLenth)) {

                        var memoryStream = new MemoryStream(userParametersBlob);
                        using (var propertyReader = new EndianBinaryReader(memoryStream)) {

                            byte[] reserved = propertyReader.ReadBytes(96);
                            byte[] signature = propertyReader.ReadBytes(2);

                            char[] signatureCharacters = Encoding.Unicode.GetChars(signature);
                            // Is it Unicode "P"
                            if ((signatureCharacters.Length == 1) && (signatureCharacters[0] == 'P')) {
                                ushort tsPropertyCount = propertyReader.ReadUInt16();

                                // remaining data is array of TSProperty structures

                                // NameLength (2 bytes): A 2-byte unsigned integer indicating the length of the PropName field in bytes.
                                // ValueLength (2 bytes): A 2-byte unsigned integer indicating the length of the PropValue field in bytes.
                                // Type (2 bytes): A 2-byte unsigned integer indicating the type of the PropValue field. 

                                for (int index = 0; index < tsPropertyCount; index++) {

                                    ushort nameLength = propertyReader.ReadUInt16();
                                    ushort valueLength = propertyReader.ReadUInt16();
                                    // valueType seems to always be 1
                                    ushort valueType = propertyReader.ReadUInt16();

                                    // PropName (variable): A Unicode string whose length is indicated by the NameLength field. 
                                    //  The string indicates the name of the property.
                                    // PropValue (variable): An encoded binary BLOB of length indicated by the ValueLength field. 
                                    //  This field either contains a 32-bit unsigned integer or an array of ASCII characters. 
                                    //  See Encoding and decoding PropValue field for encoding and decoding this BLOB.

                                    string propertyName = Encoding.Unicode.GetString(propertyReader.ReadBytes(nameLength));
                                    byte[] propertyValueBytes = propertyReader.ReadBytes(valueLength);

                                    var tsPropertyName = TSPropertyNames.Undefined;
                                    if (Enum.TryParse<TSPropertyNames>(propertyName, out tsPropertyName)) {
                                        char[] propertyValueChars = Encoding.ASCII.GetChars(propertyValueBytes);
                                        string propertyValueText = string.Empty;

                                        // Assign value, either uint or array of ascii characters, depending on the property
                                        switch (tsPropertyName) {
                                            case TSPropertyNames.CtxCfgPresent:
                                                // The CtxCfgPresent PropValue field must only contain value 0xB00B1E55.
                                                propertyValueText = this.DecodeTSPropertyNumber(propertyValueChars, flipCharPairs: true);

                                                if (!string.Equals(propertyValueText, "B00B1E55", StringComparison.OrdinalIgnoreCase)) {
                                                    throw new ApplicationException(
                                                        string.Format("Invalid value for CtxCfgPresent.  Received: {0} Expected: {1}", propertyValueText, "B00B1E55"));
                                                }
                                                break;

                                            case TSPropertyNames.CtxCfgFlags1:
                                                propertyValueText = this.DecodeTSPropertyNumber(propertyValueChars, flipCharPairs: true);
                                                uint ctxCfgFlags1Uint = 0;
                                                if (uint.TryParse(propertyValueText, NumberStyles.AllowHexSpecifier, CultureInfo.InvariantCulture, out ctxCfgFlags1Uint)) {
                                                    this.CtxCfgFlags1 = (CtxCfgFlags1)ctxCfgFlags1Uint;
                                                }
                                                break;

                                            case TSPropertyNames.CtxShadow:
                                                propertyValueText = this.DecodeTSPropertyNumber(propertyValueChars, flipCharPairs: true);
                                                uint ctxShadowUint = 0;
                                                if (uint.TryParse(propertyValueText, NumberStyles.AllowHexSpecifier, CultureInfo.InvariantCulture, out ctxShadowUint)) {
                                                    this.RemoteControlSettings = (RemoteControlSettings)ctxShadowUint;
                                                }
                                                break;

                                            case TSPropertyNames.CtxMaxConnectionTime:
                                                propertyValueText = this.DecodeTSPropertyNumber(propertyValueChars, flipCharPairs: true);
                                                uint ctxMaxConnectionTimeUint = 0;
                                                if (uint.TryParse(propertyValueText, NumberStyles.AllowHexSpecifier, CultureInfo.InvariantCulture, out ctxMaxConnectionTimeUint)) {
                                                    this.MaxConnectionTime = TimeSpan.FromMilliseconds(ctxMaxConnectionTimeUint);
                                                }
                                                break;

                                            case TSPropertyNames.CtxMaxDisconnectionTime:
                                                propertyValueText = this.DecodeTSPropertyNumber(propertyValueChars, flipCharPairs: true);
                                                uint ctxMaxDisconnectionTimeUint = 0;
                                                if (uint.TryParse(propertyValueText, NumberStyles.AllowHexSpecifier, CultureInfo.InvariantCulture, out ctxMaxDisconnectionTimeUint)) {
                                                    this.MaxDisconnectionTime = TimeSpan.FromMilliseconds(ctxMaxDisconnectionTimeUint);
                                                }
                                                break;

                                            case TSPropertyNames.CtxMaxIdleTime:
                                                propertyValueText = this.DecodeTSPropertyNumber(propertyValueChars, flipCharPairs: true);
                                                uint ctxMaxIdleTimeUint = 0;
                                                if (uint.TryParse(propertyValueText, NumberStyles.AllowHexSpecifier, CultureInfo.InvariantCulture, out ctxMaxIdleTimeUint)) {
                                                    this.MaxIdleTime = TimeSpan.FromMilliseconds(ctxMaxIdleTimeUint);
                                                }
                                                break;

                                            case TSPropertyNames.CtxMinEncryptionLevel:
                                                propertyValueText = this.DecodeTSPropertyNumber(propertyValueChars, flipCharPairs: true);
                                                uint ctxMinEncryptionLevel = 0;
                                                if (uint.TryParse(propertyValueText, NumberStyles.AllowHexSpecifier, CultureInfo.InvariantCulture, out ctxMinEncryptionLevel)) {
                                                    // Not tested - do not have a way to set this in AD Users and Computers to confirm the values
                                                    this.CtxMinEncryptionLevel = (CtxEncryptionLevel)ctxMinEncryptionLevel;
                                                }
                                                break;

                                            case TSPropertyNames.CtxWorkDirectory:
                                                this.RemoteDesktopWorkDirectory = this.DecodeTSPropertyChars(propertyValueChars);
                                                break;

                                            case TSPropertyNames.CtxWFHomeDir:
                                                this.RemoteDesktopHomeDirectory = this.DecodeTSPropertyChars(propertyValueChars);
                                                break;

                                            case TSPropertyNames.CtxWFHomeDirDrive:
                                                this.RemoteDesktopHomeDrive = this.DecodeTSPropertyChars(propertyValueChars);
                                                break;

                                            case TSPropertyNames.CtxWFProfilePath:
                                                this.RemoteDesktopProfilePath = this.DecodeTSPropertyChars(propertyValueChars);
                                                break;

                                            case TSPropertyNames.CtxInitialProgram:
                                                this.RemoteDesktopInitialProgram = this.DecodeTSPropertyChars(propertyValueChars);
                                                break;

                                            default:

                                                break;

                                        } // switch
                                    } // if (Enum.TryParse<TSPropertyNames>(propertyName, out tsPropertyName)) {
                                } // for (int index = 0; index < tsPropertyCount; index++) {
                            } // if ((signatureCharacters.Length == 1) && (signatureCharacters[0] == 'P')) {
                            else {
                                Debug.WriteLine(string.Format("{0}  [ThreadID: {1}]  {2} {3} Error: Signature of UserParameters attribute is invalid.",
                                    DateTime.Now.TimeOfDay, Thread.CurrentThread.ManagedThreadId, CustomExtensions.CurrentMethodName(),
                                    this.ParentObject ?? "N/A"));
                                this.IsValid = false;
                            }
                        } // using (var propertyReader = new EndianBinaryReader(memoryStream)) {
                    } // if ((userParametersBlob != null) && (userParametersBlob.Length > userParametersBlobMinimumLenth)) {
                    else {
                        this.IsValid = false;
                    }
                } // if (attributes.ContainsKey("userParameters")) {
            }
            catch (Exception e) {
                Debug.WriteLine(string.Format("{0}  [ThreadID: {1}]  {2} {3} Error: {4}", 
                    DateTime.Now.TimeOfDay, Thread.CurrentThread.ManagedThreadId, CustomExtensions.CurrentMethodName(), 
                    this.ParentObject ?? "N/A", e.VerboseExceptionString()));
                this.IsValid = false;
                //throw;
            }
        }

        public override string ToString() {
            var info = new StringBuilder();

            info.AppendFormat("ParentObject: {0}; ", this.ParentObject ?? "N/A");
            info.AppendFormat("IsPresent: {0}; ", this.IsPresent);
            info.AppendFormat("IsValid: {0}; ", this.IsValid);
            info.AppendFormat("IsDefaultSettings: {0}; ", this.IsDefaultSettings);

            info.AppendFormat("AllowLogon: {0}; ", this.AllowLogon);

            if (!string.IsNullOrWhiteSpace(this.RemoteDesktopHomeDirectory)) {
                info.AppendFormat("RemoteDesktopHomeDirectory: {0}; ", this.RemoteDesktopHomeDirectory);
            }
            if (!string.IsNullOrWhiteSpace(this.RemoteDesktopHomeDrive)) {
                info.AppendFormat("RemoteDesktopHomeDrive: {0}; ", this.RemoteDesktopHomeDrive);
            }
            if (!string.IsNullOrWhiteSpace(this.RemoteDesktopProfilePath)) {
                info.AppendFormat("RemoteDesktopProfilePath: {0}; ", this.RemoteDesktopProfilePath);
            }

            info.AppendFormat("RemoteControlSettings: {0}; ", this.RemoteControlSettings);

            info.AppendFormat("DisconnectAction: {0}; ", this.DisconnectAction);
            info.AppendFormat("ReconnectAction: {0}; ", this.ReconnectAction);
            if (this.MaxConnectionTime != TimeSpan.Zero) {
                info.AppendFormat("MaxConnectionTime: {0}; ", this.MaxConnectionTime.DHMSFriendlyString());
            }
            if (this.MaxDisconnectionTime != TimeSpan.Zero) {
                info.AppendFormat("MaxDisconnectionTime: {0}; ", this.MaxDisconnectionTime.DHMSFriendlyString());
            }
            if (this.MaxIdleTime != TimeSpan.Zero) {
                info.AppendFormat("MaxIdleTime: {0}; ", this.MaxIdleTime.DHMSFriendlyString());
            }
            info.AppendFormat("ConnectClientDrivesAtLogon: {0}; ", this.ConnectClientDrivesAtLogon);
            info.AppendFormat("ConnectClientPrintersAtLogon: {0}; ", this.ConnectClientPrintersAtLogon);
            info.AppendFormat("DefaultToMainPrinter: {0}; ", this.DefaultToMainPrinter);
            if (!string.IsNullOrWhiteSpace(this.RemoteDesktopInitialProgram)) {
                info.AppendFormat("RemoteDesktopInitialProgram: {0}; ", this.RemoteDesktopInitialProgram);
            }
            if (!string.IsNullOrWhiteSpace(this.RemoteDesktopWorkDirectory)) {
                info.AppendFormat("RemoteDesktopWorkDirectory: {0}; ", this.RemoteDesktopWorkDirectory);
            }

            return info.ToString();
        }
        #endregion

    }
}
